package deadlock

import (
	"fmt"
	"github.com/petermattis/goid"
	"io"
	"sort"
	"sync"
	"time"
)

type StackRecord struct {
	Gid   int64
	Stack []uintptr
	T0    int64
}

type lockerCache struct {
	startT time.Time
	lst    map[int64]StackRecord
}

type lockHolder struct {
	lk  sync.RWMutex
	lst map[interface{}]*lockerCache

	opera chan func()
}

var (
	lh *lockHolder
)

func init() {
	lh = &lockHolder{lst: make(map[interface{}]*lockerCache), opera: make(chan func(), 1024)}
	go lh.startOperator()
}

func GetLockHolderStackList(obj interface{}) (stacklst []StackRecord) {
	lst := lh.getLockHolderStackList(obj)
	sort.Slice(lst, func(i, j int) bool {
		return lst[i].T0 < lst[j].T0
	})
	return lst
}

func GetAllStackList() (stacklst []StackRecord) {
	lst := lh.getAllStacklist()
	sort.Slice(lst, func(i, j int) bool {
		return lst[i].T0 < lst[j].T0
	})
	return lst
}

func GetPotentialDeadlockList() (lst []interface{}) {
	return lh.getPotentialDeadlockList()
}

func (this *lockHolder) getLockHolderStackList(obj interface{}) (stacklst []StackRecord) {
	this.lk.RLock()
	defer this.lk.RUnlock()
	cache := this.lst[obj]
	if cache != nil {
		lst := make([]StackRecord, 0, len(cache.lst))
		for _, rec := range cache.lst {
			lst = append(lst, rec)
		}
		return lst
	}
	return nil
}

func (this *lockHolder) getAllStacklist() (stacklst []StackRecord) {
	this.lk.RLock()
	defer this.lk.RUnlock()
	for _, v := range this.lst {
		for _, v0 := range v.lst {
			stacklst = append(stacklst, v0)
		}
	}
	return stacklst
}

func (this *lockHolder) getPotentialDeadlockList() (lst []interface{}) {

	type findCache struct {
		obj    interface{}
		startT time.Time
	}

	this.lk.RLock()
	defer this.lk.RUnlock()
	if len(this.lst) == 0 {
		return nil
	}
	cacheLst := make([]findCache, 0, len(this.lst))
	for k, v := range this.lst {
		cacheLst = append(cacheLst, findCache{obj: k, startT: v.startT})
	}

	sort.Slice(cacheLst, func(i, j int) bool {
		return cacheLst[i].startT.Before(cacheLst[j].startT)
	})
	for _, v := range cacheLst {
		lst = append(lst, v.obj)
	}
	return lst
}

func (this *lockHolder) afterLock(obj interface{}, rec StackRecord) {
	this.opera <- func() {
		this.lk.Lock()
		defer this.lk.Unlock()
		data := this.lst[obj]
		if data == nil {
			data = &lockerCache{startT: time.Now(), lst: make(map[int64]StackRecord)}
			this.lst[obj] = data
		}
		data.lst[rec.Gid] = rec

	}
}

func (this *lockHolder) startOperator() {
	for {
		select {
		case op := <-this.opera:
			op()
		}
	}
}

func (this *lockHolder) afterUnlock(obj interface{}) {
	gid := goid.Get()
	this.opera <- func() {
		this.lk.Lock()
		defer this.lk.Unlock()
		data := this.lst[obj]
		if data != nil {
			delete(data.lst, gid)
			if len(data.lst) == 0 {
				delete(this.lst, obj)
			}
		}
	}
}

func DumpAllStackList(w io.Writer) {
	lst := GetAllStackList()
	if len(lst) == 0 {
		return
	}
	stacksMap := GetAllStacks()

	fmt.Fprintf(w, "The list of goroutine that hold the lock\n")
	PrintStackRecordList(w, lst, stacksMap)
}

func PrintDeadLockStack(w io.Writer, obj interface{}, stack *StackRecord) {
	lst := GetLockHolderStackList(obj)
	if len(lst) == 0 {
		return
	}

	stacksMap := GetAllStacks()

	if stack != nil {
		fmt.Fprintf(w, "current goroutine %d, %s\t", stack.Gid, time.UnixMilli(stack.T0))
		PrintStackRecord(w, stack, stacksMap)
		fmt.Fprintln(w)
		fmt.Fprintln(w)
	}

	fmt.Fprintf(w, "The list of goroutine that hold the lock %p\n", obj)
	PrintStackRecordList(w, lst, stacksMap)
}

func PrintStackRecord(w io.Writer, v *StackRecord, allStacks map[int64][]byte) {
	fmt.Fprintf(w, "goroutine %d, %s\t", v.Gid, time.UnixMilli(v.T0))
	printStack(w, v.Stack)
	fmt.Fprintln(w)
	fmt.Fprintf(w, string(allStacks[v.Gid]))
}

func PrintStackRecordList(w io.Writer, lst []StackRecord, allStacks map[int64][]byte) {
	for _, v := range lst {
		PrintStackRecord(w, &v, allStacks)
		fmt.Fprintln(w)
		fmt.Fprintln(w)
	}
}
